/* ****************************************************************************
 *
 * Copyright (c) Microsoft Corporation. 
 *
 * This source code is subject to terms and conditions of the Microsoft Public License. A
 * copy of the license can be found at http://go.microsoft.com/fwlink/?LinkID=131993. If
 * you cannot locate the Microsoft Public License, please send an email to
 * mixon@microsoft.com. By using this source code in any fashion, you are agreeing to
 * be bound by the terms of the Microsoft Public License.
 *
 * You must not remove this notice, or any other, from this software.
 *
 *
 * ***************************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;


namespace MIXOnline.Descry
{

    /// <summary>
    /// This control manages databinding its 3 SurfacePanel children to a datasource. 
    /// It also enables animation of these SurfacePanel children SurfacePanels using a mouse.
    /// It also manages a single scrollbar that acts as a scrollbar for all 3 SurfacePanels.
    /// A template in the Generic.xaml file defines the layout for the VirtualizedTimelineItemsControl
    /// </summary>
    [TemplatePart(Name = "ItemsPanels", Type = typeof(Grid))]
    [TemplatePart(Name = "VerticalScrollBar", Type = typeof(ScrollBar))]
    public class VirtualizedTimelineItemsControl : Control, ITimelineGenerateData, IMousePhysics
    {
        public VirtualizedTimelineItemsControl()
        {
            this.DefaultStyleKey = typeof(VirtualizedTimelineItemsControl); // triggers which style to load from Generic.xaml file
            this.SizeChanged += new SizeChangedEventHandler(VirtualizedItemsControl_SizeChanged);

            // inline event handler for MouseWheel
            new MouseWheelHelper(this).Moved += delegate(object sender, MouseWheelEventArgs e)
            {
                    if (e.Delta > 0)
                    {
                        this.CurrentScrollIndex -= 5;
                        e.Handled = true;
                    }
                    else if (e.Delta < 0)
                    {
                        this.CurrentScrollIndex += 5;
                        e.Handled = true;
                    }
            };

            // add support for smooth animation of timeline
            CompositionTarget.Rendering += delegate(object sender, EventArgs e)
            {
                CompositionTarget_Rendering(sender, e);
            };
        }

        #region Properties

        public bool IsDragging
        {
            get { return _isDragging; }
            set { _isDragging = value; }
        }

        protected Grid ItemsPanels { get; private set; }

        public static readonly DependencyProperty ItemsProperty =
            DependencyProperty.Register("Items", typeof(IList), typeof(VirtualizedTimelineItemsControl),
            new PropertyMetadata((d, e) => ((VirtualizedTimelineItemsControl)d).OnItemsChanged((IList)e.OldValue, (IList)e.NewValue)));
        
        public static readonly DependencyProperty ItemTemplateProperty =
            DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(VirtualizedTimelineItemsControl), new PropertyMetadata(null));

        public static readonly DependencyProperty CurrentScrollIndexProperty =
            DependencyProperty.Register("CurrentScrollIndex", typeof(double), typeof(VirtualizedTimelineItemsControl),
            new PropertyMetadata((d, e) => ((VirtualizedTimelineItemsControl)d).OnCurrentScrollIndexChanged((double)e.OldValue, (double)e.NewValue)));

        public static readonly DependencyProperty ViewProperty =
            DependencyProperty.Register("View", typeof(View), typeof(VirtualizedTimelineItemsControl),
            new PropertyMetadata((d, e) => ((VirtualizedTimelineItemsControl)d).OnViewChanged((View)e.OldValue, (View)e.NewValue)));

        public View View
        {
            get { return (View)GetValue(ViewProperty); }
            set { SetValue(ViewProperty, value); }
        }

        public IList Items
        {
            get { return (IList)GetValue(ItemsProperty); }
            set { SetValue(ItemsProperty, value); }
        }

        public DataTemplate ItemTemplate
        {
            get { return (DataTemplate)GetValue(ItemTemplateProperty); }
            set { SetValue(ItemTemplateProperty, value); }
        }

        /// <summary>
        /// Gets or sets the crrent scroll index. A step of 1 means one page
        /// </summary>
        /// <value>The index of the current scroll.</value>
        public double CurrentScrollIndex
        {
            get { return (double)GetValue(CurrentScrollIndexProperty); }
            set { SetValue(CurrentScrollIndexProperty, value); }
        }

        #endregion

        #region Events

        public event EventHandler<ScrolledEventArgs> Scrolled;

        /// <summary>
        /// Adjust panel postions when the control is resized
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void VirtualizedItemsControl_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (MiddleSurfacePanel != null)
            {
                // init positions
                double middlePosition = HelperMethods.getX(MiddleSurfacePanel);

                double width = ItemsPanels.ActualWidth;
                HelperMethods.setX(LeftSurfacePanel, middlePosition - width);
                HelperMethods.setX(RightSurfacePanel, middlePosition + width);
            }
        }

        /// <summary>
        /// Completely rebuild the SurfacePanels when the view changes
        /// </summary>
        /// <param name="oldValue"></param>
        /// <param name="newValue"></param>
        private void OnViewChanged(View oldValue, View newValue)
        {
            if (ItemsPanels != null)
            {
                FullSurfacePanelDraw();
            }
        }

        /// <summary>
        /// Redraw the Virtualized Panel with currently visible EntryItems when ScrollBar is scrolled
        /// </summary>
        /// <param name="oldValue"></param>
        /// <param name="newValue"></param>
        private void OnCurrentScrollIndexChanged(double oldValue, double newValue)
        {
            if (newValue <= this.verticalScrollBar.Minimum) newValue = this.verticalScrollBar.Minimum;
            if (newValue >= this.verticalScrollBar.Maximum) newValue = this.verticalScrollBar.Maximum;

            this.CurrentScrollIndex = newValue;

            this.verticalScrollBar.Value = newValue;
            foreach (SurfacePanel sp in ItemsPanels.Children)
            {
                foreach (object tsp in sp.Children)
                    if (tsp is VirtualizedTimelineStackPanel)
                        (tsp as VirtualizedTimelineStackPanel).InvalidateMeasure();
            }
        }

        /// <summary>
        /// Completely rebuild the SurfacePanels when a the Items property changes.
        /// </summary>
        /// <param name="oldList"></param>
        /// <param name="newList"></param>
        private void OnItemsChanged(IList oldList, IList newList)
        {
            // redraw
            if (this.ItemsPanels != null)
            {
                FullSurfacePanelDraw();
            }
        }

        #region Scrollbar Events

        /// <summary>
        /// Update Index, Fire events and Redraw the VirtualizedTimeStackPanel
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void VerticalScrollBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            var oldFirstVisibleItemIndex = this.firstVisibleItemIndex;
            this.FirstVisibleItemIndex = (int)(Math.Round(e.NewValue));
            if (oldFirstVisibleItemIndex < this.FirstVisibleItemIndex)
            {
                this.OnScrolled(new ScrolledEventArgs(Direction.Up));
            }
            if (oldFirstVisibleItemIndex > this.FirstVisibleItemIndex)
            {
                this.OnScrolled(new ScrolledEventArgs(Direction.Down));
            }
            foreach (SurfacePanel sp in ItemsPanels.Children)
            {
                foreach (object tsp in sp.Children)
                    if (tsp is VirtualizedTimelineStackPanel)
                        (tsp as VirtualizedTimelineStackPanel).InvalidateMeasure();
            }
        }

        internal virtual void OnScrolled(ScrolledEventArgs e)
        {
            var eventHandler = this.Scrolled;
            if (eventHandler != null)
            {
                eventHandler(this, e);
            }
        }

        #endregion Scrollbar Events

        #region Mouse Events and Physics

        private void ItemsPanels_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            UpdateHorizontalVelocity(e.GetPosition(ItemsPanels).X);
            _isDragging = false; // user has let go
        }

        private void ItemsPanels_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (!_isDragging)
            {
                _isDragging = true;  // user has started dragging
                UpdateMouseClick(e.GetPosition(ItemsPanels).X); // update mouse position
            }
        }

        /// <summary>
        /// Just move the Timeline according to users mouse movements if the mouse is dragging,
        /// but note the velocity of the mouse, so when the MouseLeftButtonUp event happens we
        /// want the Timeline animation move based on the last velocity of the mouse.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ItemsPanels_MouseMove(object sender, MouseEventArgs e)
        {
            if (_isDragging)
            {
                double dX = e.GetPosition(ItemsPanels).X - _lastMouseClickX; // get change in mouse position

                HelperMethods.setX(surfacePanel0, HelperMethods.getX(surfacePanel0) + dX);
                HelperMethods.setX(surfacePanel1, HelperMethods.getX(surfacePanel1) + dX);
                HelperMethods.setX(surfacePanel2, HelperMethods.getX(surfacePanel2) + dX);

                UpdateHorizontalVelocity(e.GetPosition(ItemsPanels).X); // get velocity before grid rotation

                SwapCheck();
            }
        }

        /// <summary>
        /// Returns the DateTime of the Center of the Timeline visible area
        /// </summary>
        /// <returns></returns>
        public DateTime GetTimelineCenterDateTime()
        {
            double percentage = (HelperMethods.getX(MiddleSurfacePanel) - MiddleSurfacePanel.ActualWidth / 2) / MiddleSurfacePanel.ActualWidth;
            return (MiddleSurfacePanel.TimeWindow.WindowStartTime - new TimeSpan((long)(View.TimeSpan.Ticks * percentage)));
        }

        /// <summary>
        /// This takes care of animating the Timeline after a mouse drag occurs and
        /// bases its movement on the last veloctiy _vX
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void CompositionTarget_Rendering(object sender, EventArgs e)
        {
            if (!_isDragging)
            {
                // update panel positions after mouse is released
                HelperMethods.setX(surfacePanel0, HelperMethods.getX(surfacePanel0) + _velocityX);
                HelperMethods.setX(surfacePanel1, HelperMethods.getX(surfacePanel1) + _velocityX);
                HelperMethods.setX(surfacePanel2, HelperMethods.getX(surfacePanel2) + _velocityX);
                _velocityX *= _friction;

                SwapCheck();
            }
        }

        /// <summary>
        /// Retain last mouse horizontal position
        /// </summary>
        /// <param name="currentX"></param>
        public virtual void UpdateMouseClick(double currentClickX)
        {
            _lastMouseClickX = currentClickX;
        }

        /// <summary>
        ///  Update Timeline velocity based on mouse velocity
        /// </summary>
        /// <param name="currentX">Current horizontal Mouse position</param>
        public virtual void UpdateHorizontalVelocity(double _currentClickX)
        {
            double offsetX = _currentClickX - _lastMouseClickX;
            _velocityX = offsetX * _velocitySpringyness + _velocityX * _velocityDecay;
            _lastMouseClickX = _currentClickX;
        }

        public double SurfacePanel0Position
        {
            get { return HelperMethods.getX(surfacePanel0); }
        }

        public double SurfacePanel1Position
        {
            get { return HelperMethods.getX(surfacePanel1); }
        }

        public double SurfacePanel2Position
        {
            get { return HelperMethods.getX(surfacePanel2); }
        }

        #endregion Mouse Events and Physics

        #endregion Events

        #region Overrides

        /// <summary>
        /// This override is called *only* if there is a "Themes" directory that contains a "Generic.xaml" file.
        /// When it is called we localize specific elements defined in xaml such as SurfacePanel0 
        /// and subscribe to some of their events so that the elements can then be accessed later in this code.
        /// Incidentally this file should be defined as a Resource in a Visual Studio project to prevent some errors
        /// </summary>
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            this.ItemsPanels = this.GetTemplateChild("ItemsPanels") as Grid;

            if (this.ItemsPanels == null)
            {
                throw new InvalidOperationException("Template for VirtualizedTimelineItemsControl must contain a VirtualizingPanel named ItemsPanels.");
            }

            surfacePanel0 = ItemsPanels.FindName("SurfacePanel0") as SurfacePanel;
            surfacePanel1 = ItemsPanels.FindName("SurfacePanel1") as SurfacePanel;
            surfacePanel2 = ItemsPanels.FindName("SurfacePanel2") as SurfacePanel;

            // init swappable containers
            LeftSurfacePanel = surfacePanel0;
            MiddleSurfacePanel = surfacePanel1;
            RightSurfacePanel = surfacePanel2;

            ItemsPanels.MouseMove += new MouseEventHandler(ItemsPanels_MouseMove);
            ItemsPanels.MouseLeftButtonDown += new MouseButtonEventHandler(ItemsPanels_MouseLeftButtonDown);
            ItemsPanels.MouseLeftButtonUp += new MouseButtonEventHandler(ItemsPanels_MouseLeftButtonUp);

            this.verticalScrollBar = this.GetTemplateChild("VerticalScrollBar") as ScrollBar;
            if (verticalScrollBar == null)
            {
                throw new InvalidOperationException("Template for VirtualizedTimelineItemsControl must contain a ScrollBar named VerticalScrollBar.");
            }
            this.verticalScrollBar.ValueChanged += new RoutedPropertyChangedEventHandler<double>(VerticalScrollBar_ValueChanged);
        }

        #endregion Overrides

        #region ITimelineDataGenerator

        #region DataGenerator Properties

        private int firstVisibleItemIndex;
        public int FirstVisibleItemIndex
        {
            get { return firstVisibleItemIndex; }
            protected set
            {
                this.firstVisibleItemIndex = value;
                this.verticalScrollBar.Value = value;
            }
        }

        private int fullyVisibleItemCount;
        public int FullyVisibleItemCount
        {
            get { return this.fullyVisibleItemCount; }
            set
            {
                this.fullyVisibleItemCount = value;
                this.verticalScrollBar.ViewportSize = value;
                this.verticalScrollBar.LargeChange = value;
                this.UpdateVerticalScrollBar();
            }
        }

        private Dictionary<int, int> totalItemsCount = new Dictionary<int, int>();
        public Dictionary<int, int> TotalItemsCount
        {
            get { return this.totalItemsCount; }
            private set
            {
                this.totalItemsCount = value;
                this.UpdateVerticalScrollBar();
            }
        }

        #endregion DataGenerator Properties

        public virtual UIElement GetContainerForItemOverride()
        {
            return new ContentPresenter();
        }

        /// <summary>
        /// Populate the ContentPresenter with the content. In this case an EntryItem
        /// it has been left generic so that other items can be bound to it.
        /// The content presenter is also assigned a TranslateTransform so that it
        /// can be moved inside the grid that it resides in.
        /// </summary>
        /// <param name="container"></param>
        /// <param name="dataItem"></param>
        public virtual void PrepareContainerForItemOverride(UIElement container, object dataItem)
        {
            var containerContentPresenter = container as ContentPresenter;
            if (containerContentPresenter != null && dataItem != null)
            {
                containerContentPresenter.ContentTemplate = this.ItemTemplate;
                containerContentPresenter.Content = dataItem;
                containerContentPresenter.DataContext = dataItem;
                containerContentPresenter.RenderTransform = new TranslateTransform(); // needed to add translation support of items
            }
        }

        public virtual void ClearContainerForItemOverride(UIElement container, object dataItem)
        {
            // TODO: Disconnect any events from the dataItem
        }

        public virtual void RepositionEntryItemsWithinTimeStackPanel(VirtualizedTimelineStackPanel vtsp)
        {
            if (vtsp.Children.Count > 0)
            {
                double stackpanelWidth = vtsp.ActualWidth - 50; // 50 to account for width of item
                DateTime stackpanelStartDateTime = vtsp.TimeWindow.WindowStartTime;
                long ticks = vtsp.TimeWindow.WindowTimeSpan.Ticks;

                foreach (ContentPresenter cp in vtsp.Children)
                {
                    Entry entry = cp.DataContext as Entry;

                    double pos = stackpanelWidth * (entry.Published - stackpanelStartDateTime).Ticks / ticks;

                    if (ticks > 0)
                        HelperMethods.setX(cp, pos);
                }
            }
        }

        #endregion ITimelineDataGenerator

        #region Methods

        /// <summary>
        /// Does a full redraw of all SurfacePanels. It's used when the View changes or new data is provided.
        /// </summary>
        private void FullSurfacePanelDraw()
        {
            // init SurfacePanels with timestamps appropriate to view
            DateTime StartingTime = Views.GetStartingTimeForView(View); // SurfacePanel Start time changes depending on view
            surfacePanel0.TimeWindow = new TimeWindow() { WindowStartTime = StartingTime - View.TimeSpan, WindowTimeSpan = View.TimeSpan };
            surfacePanel1.TimeWindow = new TimeWindow() { WindowStartTime = StartingTime, WindowTimeSpan = View.TimeSpan };
            surfacePanel2.TimeWindow = new TimeWindow() { WindowStartTime = StartingTime + View.TimeSpan, WindowTimeSpan = View.TimeSpan };

            RebuildAllSurfacePanels(); //destroy children, recreate, bind and redraw

            // postion panels adjacent to one another
            InitializeSurfacePanelPositions();
        }
        
        /// <summary>
        /// This function determines if the SurfacePanels should be swapped based on their postion.
        /// </summary>
        private void SwapCheck()
        {
            double middleGridPosition = HelperMethods.getX(MiddleSurfacePanel);
            double timeGridContainerWidth = ItemsPanels.ActualWidth;
            if (middleGridPosition < -timeGridContainerWidth / 2 || middleGridPosition > timeGridContainerWidth / 2)
                Swap(middleGridPosition, timeGridContainerWidth);
        }

        /// <summary>
        /// This important function swaps (or rotates) the 3 SurfacePanels to make it appear to the user that
        /// the Timeline is infinite in length
        /// </summary>
        /// <param name="middleGridPosition"></param>
        /// <param name="containerWidth"></param>
        private void Swap(double middleGridPosition, double containerWidth)
        {
            SurfacePanel OriginalLeftSurfacePanel = LeftSurfacePanel;
            SurfacePanel OriginalMiddleSurfacePanel = MiddleSurfacePanel;
            SurfacePanel OriginalRightSurfacePanel = RightSurfacePanel;

            // when moving to the left (scrolling into future)
            // rotate grid to right and update with new content
            if (middleGridPosition < -containerWidth / 2)
            {
                // reposition LeftGrid one unit to the Right of RightGrid
                HelperMethods.setX(LeftSurfacePanel, HelperMethods.getX(RightSurfacePanel) + containerWidth);

                //swap panels using third containers
                LeftSurfacePanel = OriginalMiddleSurfacePanel;
                MiddleSurfacePanel = OriginalRightSurfacePanel;
                RightSurfacePanel = OriginalLeftSurfacePanel;

                if (!DateTime.Equals(OriginalRightSurfacePanel.TimeWindow.WindowStartTime, new DateTime()))
                {
                    // increment TimeStamp on rotated panel
                    RightSurfacePanel.TimeWindow.WindowStartTime = OriginalRightSurfacePanel.TimeWindow.WindowStartTime + View.TimeSpan; //increment date
                    RightSurfacePanel.UpdateChildrenWithTimeWindow();

                    // Update this panel with new view data
                    UpdateOneSurfacePanel(RightSurfacePanel);
                }
            }

            // when moving to right (scrolling into past) 
            // rotate grid to left and update with new content.
            if (middleGridPosition > containerWidth / 2)
            {
                // reposition RightGrid one unit to the Left of LeftGrid
                HelperMethods.setX(RightSurfacePanel, HelperMethods.getX(LeftSurfacePanel) - containerWidth);

                // swap panels using third containers
                LeftSurfacePanel = OriginalRightSurfacePanel;
                MiddleSurfacePanel = OriginalLeftSurfacePanel;
                RightSurfacePanel = OriginalMiddleSurfacePanel;

                if (!DateTime.Equals(OriginalLeftSurfacePanel.TimeWindow.WindowStartTime, new DateTime()))
                {
                    // decrement TimeStamp on rotated panel
                    LeftSurfacePanel.TimeWindow.WindowStartTime = OriginalLeftSurfacePanel.TimeWindow.WindowStartTime - View.TimeSpan; // decrement date 
                    LeftSurfacePanel.UpdateChildrenWithTimeWindow();

                    // Update this panel with new view data
                    UpdateOneSurfacePanel(LeftSurfacePanel);
                }
            }
        }
        
        /// <summary>
        /// Rebind new data to all SurfacePanels and recalc Item totals per column
        /// </summary>
        private void RebuildAllSurfacePanels()
        {
            foreach (SurfacePanel surfacepanel in ItemsPanels.Children)
            {
                RebuildColumns(surfacepanel);
                surfacepanel.UpdateChildrenWithTimeWindow(); // propagates TimeWindow to children
                AssignColumnsNewDataContextAndInvalidate(surfacepanel);
            }
            RecalcAllItemCounts();
        }

        /// <summary>
        /// Rebind new data to a SurfacePanel and recalc Item totals per column
        /// </summary>
        /// <param name="surfacepanel"></param>
        private void UpdateOneSurfacePanel(SurfacePanel surfacepanel)
        {
            AssignColumnsNewDataContextAndInvalidate(surfacepanel);
            RecalcAllItemCounts();
        }

        /// <summary>
        /// Programmatically build a SurfacePanel from several VirtualizedTimeStackPanels
        /// according to the current view which defines the number of intervals. For example
        /// when in "week" view there would be 7 intervals. One for every day of the week. 
        /// </summary>
        /// <param name="surfacepanel"></param>
        private void RebuildColumns(SurfacePanel surfacepanel)
        {
            surfacepanel.Children.Clear(); //TODO: remove events so old VirtualizedTimeStackPanels can be garbage collected properly.
            surfacepanel.ColumnDefinitions.Clear();

            for (int count = 0; count < this.View.Intervals; count++)
            {
                surfacepanel.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star ) });
                VirtualizedTimelineStackPanel vtsp = new VirtualizedTimelineStackPanel();
                vtsp.SizeChanged += new SizeChangedEventHandler(StackPanel_SizeChanged);
                Grid.SetColumn(vtsp, count);
                surfacepanel.Children.Add(vtsp);

                // hard coded color of single pixel stripe
                Canvas c = new Canvas() { Background = new SolidColorBrush(Color.FromArgb(0xff, 0x26, 0x26, 0x26)), Width = 1, HorizontalAlignment = HorizontalAlignment.Left };
                Grid.SetColumn(c, count);
                surfacepanel.Children.Add(c);
            }
        }

        /// <summary>
        /// Determine what data should be bound to this panel based on
        /// the TimeWindow 
        /// </summary>
        /// <param name="surfacepanel">The SurfacePanel that should be updated</param>
        private void AssignColumnsNewDataContextAndInvalidate(SurfacePanel surfacepanel)
        {
            foreach (object o in surfacepanel.Children)
            {
                if (o is VirtualizedTimelineStackPanel)
                {
                    VirtualizedTimelineStackPanel vtsp = o as VirtualizedTimelineStackPanel;
                    DateTime startingDateTime = vtsp.TimeWindow.WindowStartTime;
                    TimeSpan columnTimeSpan = vtsp.TimeWindow.WindowTimeSpan;

                    // Filter EntryItem's that fit in this TimeStackPanel
                    List<Entry> filteredlist = (from p in this.Items.Cast<Entry>() as IEnumerable<Entry>
                                                where p.Published >= startingDateTime && p.Published < startingDateTime + columnTimeSpan
                                                select p).ToList();

                    vtsp.DataContext = filteredlist; // this is what defines which subset of items reside in this specific VirtualizedTimeStackPanel

                    if (vtsp.DataGenerator == null) vtsp.DataGenerator = this; // if not bound, bind it

                    vtsp.InvalidateMeasure(); // trigger MeasureOveride
                }
            }
        }

        /// <summary>
        ///  Recalculate new max TotalItemCount
        ///  A hashcode is used to uniquely indentify objects
        /// </summary>
        private void RecalcAllItemCounts()
        {
            this.TotalItemsCount.Clear();
            foreach (SurfacePanel sp in ItemsPanels.Children)
            {
                foreach (object tsp in sp.Children)
                {
                    if (tsp is VirtualizedTimelineStackPanel)
                        this.TotalItemsCount[tsp.GetHashCode()] = ((tsp as VirtualizedTimelineStackPanel).DataContext as IList).Count;
                }
            }
        }

        /// <summary>
        /// update positioning of EntryItems within the StackPanel as it is resized.
        /// </summary>
        void StackPanel_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            // when the StackPanel Changes size horizontally, we need to reposition the
            // EntryItem Elements to fit within the new width

            // if the width didn't change take no action
            if (e.PreviousSize.Width - e.NewSize.Width == 0) return;

            RepositionEntryItemsWithinTimeStackPanel(sender as VirtualizedTimelineStackPanel);
        }

        /// <summary>
        /// Position panels in their initial position
        /// </summary>
        private void InitializeSurfacePanelPositions()
        {
            // init swappable containers here ?????
            LeftSurfacePanel = surfacePanel0;
            MiddleSurfacePanel = surfacePanel1;
            RightSurfacePanel = surfacePanel2;

            // init panel positions
            double width = ItemsPanels.ActualWidth;
            HelperMethods.setX(surfacePanel0, -width);
            HelperMethods.setX(surfacePanel1, 0);
            HelperMethods.setX(surfacePanel2, width);
        }

        /// <summary>
        /// This determines how many EntryItems are in the visible area of the control and 
        /// adjusts the dimensions of the ScrollBar and turns it off if there are no items to scroll down to.
        /// </summary>
        private void UpdateVerticalScrollBar()
        {
            if (this.verticalScrollBar != null)
            {
                int totalItems = 0;

                foreach (KeyValuePair<int, int> kvp in this.TotalItemsCount)
                {
                    totalItems = Math.Max(kvp.Value, totalItems); // set the totalItems to maximum of all columns.
                }

                this.verticalScrollBar.Maximum = totalItems - this.FullyVisibleItemCount;

                if (totalItems <= this.FullyVisibleItemCount) // only show the scrollbar if there are more items than can fit in the control height.
                {
                    this.verticalScrollBar.Visibility = Visibility.Collapsed;
                }
                else
                {
                    this.verticalScrollBar.Visibility = Visibility.Visible;
                }
            }
        }

        /// <summary>
        /// Move the Timeline by dX distance
        /// </summary>
        public void MoveTimelineByPixels(double dX)
        {
            HelperMethods.setX(surfacePanel0, HelperMethods.getX(surfacePanel0) + dX);
            HelperMethods.setX(surfacePanel1, HelperMethods.getX(surfacePanel1) + dX);
            HelperMethods.setX(surfacePanel2, HelperMethods.getX(surfacePanel2) + dX);

            SwapCheck();
        }

        #endregion Methods

        #region Privates

        private SurfacePanel surfacePanel0, surfacePanel1, surfacePanel2, LeftSurfacePanel, MiddleSurfacePanel, RightSurfacePanel;
        private ScrollBar verticalScrollBar;
        private bool _isDragging = false;
        private const double _velocitySpringyness = 4;     
        private const double _velocityDecay = 0.2;        
        private const double _friction = 0.90;      
        private double _velocityX = 0;
        private double _lastMouseClickX = 0;

        #endregion Privates
    }
}
